﻿using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Windows.Forms;
using EnvDTE;
using Microsoft.VisualStudio;
using Microsoft.VisualStudio.Designer.Interfaces;
using Microsoft.VisualStudio.Project;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.WowAddonStudio.Project.Automation;
using Microsoft.WowAddonStudio.Services;
using Microsoft.WowAddonStudio.TocEditor;
using VSLangProj;

namespace Microsoft.WowAddonStudio.Project
{
	/// <summary>
	/// Represents a project node in the Solution Explorer and controls its behavior.
	/// </summary>
	[Guid(GuidStrings.WowProjectNode)]
	public class WowProjectNode : ProjectNode
	{
		private const string wowPathPropertyName = "WowPath";
		private const string wowInterfaceToken = "$wowinterface$";

		private static readonly ImageList wowImageList;
		private static int wowImageOffset;

		private readonly WowProjectPackage package;
		private readonly TocDocument tocDocument;

		/// <summary>
		/// Initializes the <see cref="WowProjectNode"/> class.
		/// </summary>
		static WowProjectNode()
		{
			// Get the image list from the embedded resource used by the WoW project
			wowImageList =
				Utilities.GetImageList(
					typeof (WowProjectNode).Assembly.GetManifestResourceStream("Resources.WowImageList.bmp"));
		}

		/// <summary>
		/// Initializes a new instance of the <see cref="WowProjectNode"/> class.
		/// </summary>
		/// <param name="package">The package the project type resides in.</param>
		public WowProjectNode(WowProjectPackage package)
		{
			if (package == null)
				throw new ArgumentNullException("package");

			this.package = package;

			// File nodes can have children (!) (think Frame XML has a Lua codebehind file)
			CanFileNodesHaveChilds = true;

			// Support the Project Designer Editor
			SupportsProjectDesigner = true;

			// Allow deleting items
			CanProjectDeleteItems = true;

			// Store the number of images before we add our own so we know the offset where we start
			wowImageOffset = base.ImageHandler.ImageList.Images.Count;
			OleServiceProvider.AddService(typeof (VSProject), new OleServiceProvider.ServiceCreatorCallback(CreateServices), false);

			// Add all images
			foreach (Image image in WowImageList.Images)
				base.ImageHandler.AddImage(image);

			//Read content of toc file (project properties)
			tocDocument = new TocDocument();

            // Add CATID mapping
            this.AddCATIDMapping(typeof(TocEditorAddOnInfoPage), typeof(WowProjectNodeProperties).GUID);
            this.AddCATIDMapping(typeof(TocEditorFilesPage), typeof(WowProjectNodeProperties).GUID);
		}

		/// <summary>
		/// Provides a list of files under the project node without
		/// the toc file in which project properties are stored.
		/// </summary>
		public List<string> GetRelativeFilePathsWithoutToc()
		{
			return
				new List<string>(GetRelativeFilePathsWithoutToc(this).Where(relativePath => !GetTocFilePath().EndsWith(relativePath))
				                 	.
				                 	OrderByDescending(relativePath => relativePath));
		}

		/// <summary>
		/// Gets the relative file paths without toc.
		/// </summary>
		/// <param name="rootNode">The root node.</param>
		/// <returns></returns>
		private List<string> GetRelativeFilePathsWithoutToc(HierarchyNode rootNode)
		{
			var result = new List<string>();

			HierarchyNode workingNode = rootNode.FirstChild;

			while (workingNode != null)
			{
				if (workingNode is FileNode)
					result.Add(ExtractRelativePath(workingNode.Url));

				result.AddRange(GetRelativeFilePathsWithoutToc(workingNode));
				workingNode = workingNode.NextSibling;
			}

			return result;
		}

		/// <summary>
		/// Extracts the relative path.
		/// </summary>
		/// <param name="fullPath">The full path.</param>
		/// <returns></returns>
		private string ExtractRelativePath(string fullPath)
		{
			//TODO: Implement a more sophisticates relative path algorithm
			string result = fullPath.Substring(ProjectFolder.Length);
			if (result.IndexOf(Path.DirectorySeparatorChar) == 0)
				result = result.Substring(1);

			return result;
		}

		/// <summary>
		/// Initializes the toc document.
		/// </summary>
		private void InitializeTocDocument()
		{
			if (String.IsNullOrEmpty(GetTocFilePath()))
			{
				CreateMissingTocFile();

				//Create toc file, and add it to project
				AddNewFileNodeToHierarchy(this, GetTocFilePath());
			}
			TocDocument.Read(GetTocFilePath());
		}

		/// <summary>
		/// Creates the missing toc file.
		/// </summary>
		private void CreateMissingTocFile()
		{
			string tocPath = Path.Combine(ProjectFolder, Path.GetFileNameWithoutExtension(ProjectFile) + ".toc");

			using (File.CreateText(tocPath))
			{
			}
		}

		/// <summary>
		/// Creates the services exposed by this project.
		/// </summary>
		private object CreateServices(Type serviceType)
		{
            if (serviceType == typeof(EnvDTE.Project))
                return GetAutomationObject();

			return GetService(serviceType);
		}

		/// <summary>
		/// Returns an automation object representing this node
		/// </summary>
		/// <returns>The automation object</returns>
		public override object GetAutomationObject()
		{
			return new OAWowProject(this);
		}

		/// <summary>
		/// Gets the image list that contains the WoW icons for the nodes.
		/// </summary>
		public static ImageList WowImageList
		{
			get { return wowImageList; }
		}

		/// <summary>
		/// Gets the index of the image to show for the project node.
		/// </summary>
		public override int ImageIndex
		{
			get
			{
				// We need to add the offset as we appended our own images to an existing image list.
				return wowImageOffset + (int) WowImage.WowProject;
			}
		}

		/// <summary>
		/// Gets the Guid of the project factory.
		/// </summary>
		public override Guid ProjectGuid
		{
			get { return typeof (WowProjectFactory).GUID; }
		}

		/// <summary>
		/// Gets the type of the project.
		/// </summary>
		public override string ProjectType
		{
			get { return GetType().Name; }
		}

		/// <summary>
		/// Retrieves the toc document instance belonging to this project node.
		/// </summary>
		public TocDocument TocDocument
		{
			get { return tocDocument; }
		}

		/// <summary>
		/// Saves the TOC document.
		/// </summary>
		public void SaveTocDocument()
		{
			try
			{
				TocDocument.Write(GetTocFilePath());
			}
			catch (IOException ex)
			{
				MessageBox.Show(
					String.Format("The toc file could not be saved due to: {0}", ex.Message),
					"Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
			}
		}

		/// <summary>
		/// 
		/// </summary>
		//internal override object Object
		//{
		//    get
		//    {
		//        return this.VSProject;
		//    }
		//}
		/// <summary>
		/// Loads a project file. Called from the factory CreateProject to load the project.
		/// </summary>
		/// <param name="fileName">File name of the project that will be created. </param>
		/// <param name="location">Location where the project will be created.</param>
		/// <param name="name">If applicable, the name of the template to use when cloning a new project.</param>
		/// <param name="flags">Set of flag values taken from the VSCREATEPROJFLAGS enumeration.</param>
		/// <param name="iidProject">Identifier of the interface that the caller wants returned. </param>
		/// <param name="canceled">An out parameter specifying if the project creation was canceled</param>
		public override void Load(string fileName, string location, string name, uint flags, ref Guid iidProject,
		                          out int canceled)
		{
			// Let MPF first deal with the tasks at hand
			base.Load(fileName, location, name, flags, ref iidProject, out canceled);

            // Initialize the TOC document
			InitializeTocDocument();

            // Register hierarchy with library manager
            var libraryManager = Site.GetService(typeof(ILibraryManager)) as ILibraryManager;
            if (libraryManager != null)
                libraryManager.RegisterHierarchy(this.InteropSafeHierarchy);
		}

        public override int Close()
        {
            if (Site != null)
            {
                var libraryManager = Site.GetService(typeof(ILibraryManager)) as ILibraryManager;
                if (libraryManager != null)
                    libraryManager.UnregisterHierarchy(this.InteropSafeHierarchy);
            }

            return base.Close();
        }

		/// <summary>
		/// In this override, before the build takes place,
		/// set MSBuild project properties, namely the WowPath property.
		/// </summary>
		public override MSBuildResult Build(uint vsopts, string config, IVsOutputWindowPane output, string target)
		{
			var locator = package.GetService<IWowLocatorService>();

			if (locator != null)
			{
				SetProjectProperty(wowPathPropertyName, locator.GetWowBasePath());
			}
			//Add AddonTitle to project  properties
			if (TocDocument != null)
			{
				var addOnName = Path.GetFileNameWithoutExtension(TocDocument.TocFileFullName);
				SetProjectProperty(GeneralPropertyPageTag.AddonTitle.ToString(), addOnName);
			}

			return base.Build(vsopts, config, output, target);
		}

		/// <summary>
		/// Creates an object derived from NodeProperties that will be used to expose properties
		/// spacific for this object to the property browser.
		/// </summary>
		/// <returns></returns>
		protected override NodeProperties CreatePropertiesObject()
		{
			return new WowProjectNodeProperties(this);
		}

		/// <summary>
		/// Creates a file node for a project element.
		/// </summary>
		/// <param name="element">The project element.</param>
		/// <returns>An instance of the <see cref="FileNode"/> class.</returns>
		public override FileNode CreateFileNode(ProjectElement element)
		{
			if (element == null)
				throw new ArgumentNullException("element");

			WowFileNode node;

			// Get the SubType for the project element.
			string subType = element.GetMetadata(ProjectFileConstants.SubType);

			switch (subType)
			{
				case WowSubType.Code:
					node = new WowLuaFileNode(this, element);
					break;
				case WowSubType.Frame:
					node = new WowXmlFileNode(this, element);
					break;
				case WowSubType.TableOfContents:
					node = new WowTocFileNode(this, element);
					break;
				default:
					// We could not recognize the file subtype, just create a WowFileNode
					node = new WowFileNode(this, element);
					break;
			}

			// Check whether this file should be added to the language service
			if (subType == WowSubType.Frame || subType == WowSubType.Code)
			{
				node.OleServiceProvider.AddService(
					typeof (SVSMDCodeDomProvider), new OleServiceProvider.ServiceCreatorCallback(CreateServices), false);
				node.OleServiceProvider.AddService(typeof (EnvDTE.Project),
				                                   new OleServiceProvider.ServiceCreatorCallback(CreateServices), false);
				node.OleServiceProvider.AddService(typeof (ProjectItem), node.ServiceCreator, false);
			}

			return node;
		}

		/// <summary>
		/// Creates a file node for a project element.
		/// </summary>
		/// <param name="element">The project element.</param>
		/// <returns>An instance of the <see cref="DependentFileNode"/> class.</returns>
		public override DependentFileNode CreateDependentFileNode(ProjectElement element)
		{
			if (element == null)
				throw new ArgumentNullException("element");

			string subType = element.GetMetadata(ProjectFileConstants.SubType);

			if (subType == WowSubType.Code)
				return new WowLuaDependentFileNode(this, element);

			return base.CreateDependentFileNode(element);
		}

		/// <summary>
		/// Creates the reference container node.
		/// </summary>
		/// <returns></returns>
		protected override ReferenceContainerNode CreateReferenceContainerNode()
		{
			// Return null as we do not support references
			return null;
		}

		/// <summary>
		/// Adds the new file node to hierarchy.
		/// </summary>
		/// <param name="parentNode">The parent node.</param>
		/// <param name="path">Name of the file.</param>
		protected override void AddNewFileNodeToHierarchy(HierarchyNode parentNode, string path)
		{
			// If a lua file is being added, try to find a related FrameXML node
			if (IsLuaFile(path))
			{
				// Try to find a FrameXML node with a matching relational name
				string fileName = Path.GetFileNameWithoutExtension(path);
				HierarchyNode childNode = FirstChild;

				// Iterate through the children
				while (childNode != null)
				{
					// If this child is an XML node and its relational name matches, break out of the loop
					if (childNode is WowXmlFileNode &&
					    String.Compare(childNode.GetRelationalName(), fileName, StringComparison.OrdinalIgnoreCase) == 0)
					{
						parentNode = childNode;
						break;
					}

					// Move over to the next sibling
					childNode = childNode.NextSibling;
				}
			}

			HierarchyNode child;

			if (CanFileNodesHaveChilds && (parentNode is FileNode || parentNode is DependentFileNode))
			{
				child = CreateDependentFileNode(path);
				child.ItemNode.SetMetadata(ProjectFileConstants.DependentUpon,
				                           parentNode.ItemNode.GetMetadata(ProjectFileConstants.Include));

				// Make sure to set the HasNameRelation flag on the dependent node if it is related to the parent by name
				if (
					String.Compare(child.GetRelationalName(), parentNode.GetRelationalName(),
					               StringComparison.OrdinalIgnoreCase) == 0)
				{
					child.HasParentNodeNameRelation = true;
				}
			}
			else
			{
				//Create and add new filenode to the project
				child = CreateFileNode(path);
			}

			parentNode.AddChild(child);
			Tracker.OnItemAdded(path, VSADDFILEFLAGS.VSADDFILEFLAGS_NoFlags);
		}

		/// <summary>
		/// Adds the file from template.
		/// </summary>
		/// <param name="source">The source.</param>
		/// <param name="target">The target.</param>
		public override void AddFileFromTemplate(string source, string target)
		{
			if (source == null)
				throw new ArgumentNullException("source");
			if (target == null)
				throw new ArgumentNullException("target");
			if (!File.Exists(source))
				throw new FileNotFoundException(Resources.GetString(Resources.FailedToLoadTemplateFileMessage, source));

			// We assume that there is no token inside the file because the only
			// way to add a new element should be through the template wizard that
			// take care of expanding and replacing the tokens.
			// The only task to perform is to copy the source file in the
			// target location.
			string targetFolder = Path.GetDirectoryName(target);

			if (!Directory.Exists(targetFolder))
				Directory.CreateDirectory(targetFolder);

			if (File.Exists(target))
				File.Delete(target);

			if (IsTableOfContentsFile(target))
			{
				//Copy and replace tags
				CopyAndResolveTags(source, target);
			}
			else
			{
				//Plain copy
				File.Copy(source, target);
			}
		}

		/// <summary>
		/// Copies the and resolve tags.
		/// </summary>
		/// <param name="source">The source.</param>
		/// <param name="target">The target.</param>
		private void CopyAndResolveTags(string source, string target)
		{
			using (StreamReader sourceFile = File.OpenText(source))
			{
				string fileContent = sourceFile.ReadToEnd();
				var wowLocatorService = package.GetService<IWowLocatorService>();
				string wowInterface = wowLocatorService.GetWowInterface();

				fileContent = fileContent.Replace(wowInterfaceToken, wowInterface);

				using (StreamWriter writer = File.CreateText(target))
				{
					writer.Write(fileContent);
				}
			}
		}

		/// <summary>
		/// Creates an MSBuild ProjectElement for a file.
		/// </summary>
		/// <param name="file">The path to the file.</param>
		/// <returns>An instance of the <see cref="ProjectElement"/> class.</returns>
		protected override ProjectElement AddFileToMsBuild(string file)
		{
			if (file == null)
				throw new ArgumentNullException("file");

			string itemPath = PackageUtilities.MakeRelativeIfRooted(file, BaseURI);

			if (Path.IsPathRooted(itemPath))
				throw new ArgumentException("Cannot add item with full path.", "file");

			ProjectElement newItem = CreateMsBuildFileItem(itemPath, ProjectFileConstants.Content);

			if (IsLuaFile(itemPath))
			{
				newItem.SetMetadata(ProjectFileConstants.SubType, WowSubType.Code);
			}
			else if (IsFrameXmlFile(itemPath))
			{
				newItem.SetMetadata(ProjectFileConstants.SubType, WowSubType.Frame);
			}
			else if (IsTableOfContentsFile(itemPath))
			{
				newItem.SetMetadata(ProjectFileConstants.SubType, WowSubType.TableOfContents);
			}

			return newItem;
		}

		/// <summary>
		/// Gets the configuration independent property pages.
		/// </summary>
		/// <returns></returns>
		protected override Guid[] GetConfigurationIndependentPropertyPages()
		{
			var result = new Guid[2];
			result[0] = typeof (TocEditorAddOnInfoPage).GUID;
			result[1] = typeof (TocEditorFilesPage).GUID;
			return result;
		}


		/// <summary>
		/// Gets the format list.
		/// </summary>
		/// <param name="formatList">The format list.</param>
		/// <returns></returns>
		public override int GetFormatList(out string formatList)
		{
			formatList = Resources.GetString(Resources.FormatList, "\0");
			return VSConstants.S_OK;
		}

		/// <summary>
		/// Called when a Add References.. is pressed.
		/// </summary>
		/// <returns>Returns a value of the <see cref="VSConstants"/> enumeration.</returns>
		public override int AddProjectReference()
		{
			return VSConstants.S_OK;
		}

		/// <summary>
		/// Evaluates if a file is a Lua code file based on is extension
		/// </summary>
		/// <param name="strFileName">The filename to be evaluated</param>
		/// <returns>true if is a code file</returns>
		public override bool IsCodeFile(string strFileName)
		{
			return IsLuaFile(strFileName);
		}

        /// <summary>
        /// Deletes the item.
        /// </summary>
        /// <param name="delItemOp">The del item op.</param>
        /// <param name="itemId">The item id.</param>
        /// <returns></returns>
        public override int DeleteItem(uint delItemOp, uint itemId)
        {
            var node = this.ProjectMgr.NodeFromItemId(itemId);
            if (node is WowTocFileNode)
            {
                VsShellUtilities.ShowMessageBox(this.ProjectMgr.Site, "The table of contents (TOC) file is required!", "", OLEMSGICON.OLEMSGICON_WARNING, OLEMSGBUTTON.OLEMSGBUTTON_OK, OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST);
                return VSConstants.S_OK;
            }

            return base.DeleteItem(delItemOp, itemId);
        }

		/// <summary>
		/// Determines whether [is lua file] [the specified file name].
		/// </summary>
		/// <param name="fileName">Name of the file.</param>
		/// <returns>
		/// 	<c>true</c> if [is lua file] [the specified file name]; otherwise, <c>false</c>.
		/// </returns>
		protected static bool IsLuaFile(string fileName)
		{
			if (String.IsNullOrEmpty(fileName))
				return false;

			return (String.Compare(Path.GetExtension(fileName), ".lua", StringComparison.OrdinalIgnoreCase) == 0);
		}

		/// <summary>
		/// Determines whether [is frame XML file] [the specified file name].
		/// </summary>
		/// <param name="fileName">Name of the file.</param>
		/// <returns>
		/// 	<c>true</c> if [is frame XML file] [the specified file name]; otherwise, <c>false</c>.
		/// </returns>
		protected static bool IsFrameXmlFile(string fileName)
		{
			if (String.IsNullOrEmpty(fileName))
				return false;

			// TODO: Also look into the file, might be a normal XML file

			return (String.Compare(Path.GetExtension(fileName), ".xml", StringComparison.OrdinalIgnoreCase) == 0);
		}

		/// <summary>
		/// Determines whether [is table of contents file] [the specified file name].
		/// </summary>
		/// <param name="fileName">Name of the file.</param>
		/// <returns>
		/// 	<c>true</c> if [is table of contents file] [the specified file name]; otherwise, <c>false</c>.
		/// </returns>
		protected static bool IsTableOfContentsFile(string fileName)
		{
			if (String.IsNullOrEmpty(fileName))
				return false;

			return (String.Compare(Path.GetExtension(fileName), ".toc", StringComparison.OrdinalIgnoreCase) == 0);
		}

		/// <summary>
		/// Creates the folder node.
		/// </summary>
		/// <param name="path">The path.</param>
		/// <param name="element">The element.</param>
		/// <returns></returns>
		protected internal override FolderNode CreateFolderNode(string path, ProjectElement element)
		{
			FolderNode folderNode = new WowFolderNode(this, path, element);
			return folderNode;
		}

		/// <summary>
		/// Gets the toc file path.
		/// </summary>
		/// <returns></returns>
		public string GetTocFilePath()
		{
			string tocFilePath = Path.Combine(ProjectFolder,
			                                  Path.GetFileNameWithoutExtension(ProjectFile) + ".toc");

			if (File.Exists(tocFilePath))
				return tocFilePath;

			return string.Empty;
		}

        private IVsHierarchy InteropSafeHierarchy
        {
            get
            {
                IntPtr unknownPtr = Utilities.QueryInterfaceIUnknown(this);
                if (IntPtr.Zero == unknownPtr)
                    return null;

                return Marshal.GetObjectForIUnknown(unknownPtr) as IVsHierarchy;
            }
        }
	}
}